/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */


package at.ac.tuwien.infosys.dsg.daios.wsdl.impl;

import java.net.URL;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.wsdl.Binding;
import javax.wsdl.Definition;
import javax.wsdl.Message;
import javax.wsdl.Operation;
import javax.wsdl.Part;
import javax.wsdl.Port;
import javax.wsdl.PortType;
import javax.wsdl.Service;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.soap.SOAPAddress;
import javax.wsdl.extensions.soap.SOAPBinding;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLReader;
import javax.xml.namespace.QName;

import at.ac.tuwien.infosys.dsg.daios.exceptions.AmbigiousException;
import at.ac.tuwien.infosys.dsg.daios.framework.DaiosInterceptor;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.wsdl.WSDLEncoding;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDL;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLOperation;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLPart;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLPort;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLService;
import at.ac.tuwien.infosys.dsg.daios.wsdl.parser.TypeParser;
import at.ac.tuwien.infosys.dsg.daios.wsdl.typesystem.WSDLTypeTree;

import com.ibm.wsdl.factory.WSDLFactoryImpl;

/**
 * A Daios WSDL definition structure. This method wraps a complete
 * WSDL Web Service definition.
 * 
 * @author philipp
 *
 */
public class WSDL implements IWSDL {
	
	// the URL location of the WSDL file
	private URL location = null;
	
	// the WSDL4J WSDL file
	private Definition wsdl = null;
	
	private Logger log = null;
	private WSDLEncoding style = null;
	private List<IWSDLService> services = null;
	private List<IWSDLMessage> messages = null;
	private List<IWSDLOperation> operations = null;
	private WSDLTypeTree types = null;
	private DaiosInterceptor interceptor = null;
	
	/**
	 * Create a new WSDL definition from a given WSDL file.
	 * The WSDL source file is given by its URL location.
	 * 
	 * @param location The URI of the WSDL file to parse.
	 * @throws WSDLException
	 * @throws AmbigiousException
	 */
	public WSDL(URL location) throws WSDLException,
		AmbigiousException {
		
		log = Logger.getLogger(IConstants.LOG_NAME);
		
		this.location = location;
		
		log.info("Starting new IWSDL object from definition at "
			+location.toExternalForm());
		
		// read this URL and parse a WSDL definition
		WSDLFactory fac = new WSDLFactoryImpl();
		WSDLReader reader = fac.newWSDLReader();
		// we want to follow imports
		reader.setFeature("javax.wsdl.importDocuments", true);
		
		// read the WSDL definition with WSDL4J
		wsdl = reader.readWSDL(location.toExternalForm());
		
		if(wsdl != null) {
			log.info("Successfully parsed WSDL file.");
			style  = retrieveStyle();
			log.info("Using style "+style);
		}
		
	}
	
	/**
	 * Get all services that are defined in this WSDL file.
	 * 
	 * @return A list of all services.
	 */
	public List<IWSDLService> getServices() {
		
		if(services != null)
			return services;
		else
			return services = retrieveServices();
		
	}

	/**
	 * Get all messages that are defined in this WSDL file.
	 * 
	 * @return A list of all messages.
	 */
	public List<IWSDLMessage> getMessages() {
		
		if(messages != null)
			return messages;
		else
			return messages = retrieveMessages();
		
	}

	/**
	 * Get a specific message from this definition.
	 * 
	 * @param message The QName of the message to get.
	 * @return The message. Null if such a message does not
	 * exist in this definition.
	 */	
	public IWSDLMessage getMessage(QName message) {
		
		// if we don't have any messages figured out yet
		// we do that
		if(messages == null)
			getMessages();
		
		// then just search for the message with this qname
		for(IWSDLMessage m: messages)
			if(m.getMessageName().equals(message.getLocalPart()))
				return m;
			
		return null;
		
	}

	/**
	 * Get all operations that are defined in this WSDL file.
	 * 
	 * @return A list of all operations.
	 */
	public List<IWSDLOperation> getOperations() {
		
		if(operations != null)
			return operations;
		else
			return operations = retrieveOperations();
		
	}
	
	/**
	 * Get a specific operation from this definition.
	 * 
	 * @param operation The QName of the operation to get.
	 * @return The operation. Null if such an operation does not
	 * exist in this definition.
	 */	
	public IWSDLOperation getOperation(QName operation) {

		// if we don't have any operations figured out yet
		// we do that
		if(operations == null)
			getOperations();
		
		// then just search for the operation with this qname
		for(IWSDLOperation op: operations)
			if(op.getName().equals(operation.getLocalPart()))
				return op;
		
		return null;
	}

	/**
	 * Get all types that are defined in this WSDL file.
	 * 
	 * @return A list of all types.
	 */
	public WSDLTypeTree getTypes() {
		
		if(types != null)
			return types;
		else
			return types = retrieveTypes();
		
	}

	/**
	 * Get the SOAP binding style of this WSDL definition.
	 * 
	 * @return The binding style (rpc or document).
	 */
	public WSDLEncoding getBindingStyle() {
		return style;
	}
	
	/**
	 * Hook an interceptor into the WSDL file.
	 * 
	 * @param interceptor
	 */
	public void setInterceptor(DaiosInterceptor interceptor) {
		this.interceptor = interceptor;
	}
	
	/**
	 * Load all services.
	 * 
	 * @return A list of all services.
	 */
	private List<IWSDLService> retrieveServices() {
		
		log.info("Getting all services.");
		
		// get all services
		Map services = wsdl.getServices();
		log.info("We have "+services.size()+" Services in this definition.");
		
		// convert all services to Daios format
		List<IWSDLService> DaiosServices = new
			LinkedList<IWSDLService>();
		List<IWSDLPort> DaiosPorts = null;	
		Service s = null;
		Port p = null;
		Map ports = null;
		String uri = null;
		SOAPAddress el = null;
		for(Object o : services.values()) {
			s = (Service) o;
			ports = s.getPorts();
			DaiosPorts = new
				LinkedList<IWSDLPort>();	
			for(Object po : ports.values()) {
				p = (Port) po;
				if(!(p.getExtensibilityElements().get(0) instanceof SOAPAddress))
						continue;
				el = (SOAPAddress)
					p.getExtensibilityElements().get(0);
				// TODO: this should work with different address types
				
				uri = el.getLocationURI();
				DaiosPorts.add(new WSDLPort(p.getName(), uri));
			}
			DaiosServices.add(new WSDLService(s.getQName().getLocalPart(),DaiosPorts));
		}
		
		return DaiosServices;
		
	}
	
	/**
	 * Load all messages.
	 * 
	 * @return A list of all messages.
	 */
	private List<IWSDLMessage> retrieveMessages() {
		
		log.info("Getting all messages.");
		
		// get all messages
		Map msgs = wsdl.getMessages();
		log.info("We have "+msgs.size()+" Messages in this definition.");
		
		// convert all messages to Daios format
		List<IWSDLMessage> DaiosMsgs = new
			LinkedList<IWSDLMessage>();
		Message m = null;
		IWSDLMessage newM = null;
		Map parts = null;
		List<IWSDLPart> DaiosParts = null;
		Part p = null;
		for(Object o : msgs.values()) {
			m = (Message) o;
			parts = m.getParts();
			DaiosParts = new
				LinkedList<IWSDLPart>();
			for(Object po : parts.values()) {
				p = (Part)po;
				
				// we have type names only in RPC encoding
				if(p.getTypeName() != null)
					DaiosParts.add(new WSDLRPCPart(
						p.getName(), p.getTypeName().getLocalPart()));
				
				// in docu style we should have a element name
				else
					DaiosParts.add(new WSDLDocumentPart(
						p.getName(), p.getElementName().getLocalPart()));					
			
			}
			
			newM = new WSDLMessage(m.getQName().getNamespaceURI(),
					m.getQName().getLocalPart(),DaiosParts);
			DaiosMsgs.add(newM);
		
		}
		
		// return this list of messages
		return DaiosMsgs;

	}

	/**
	 * Load all operations.
	 * 
	 * @return A list of all operations.
	 */
	@SuppressWarnings("unchecked")
	private List<IWSDLOperation> retrieveOperations() {
		
		log.info("Getting all operations.");
		
		// get all operations
		List ops = new LinkedList();
		Map portMap = wsdl.getPortTypes();
		Collection c = portMap.values();
		PortType port = null;
		for(Object o : c) {
			port = (PortType)o;
			ops.addAll(port.getOperations());
		}		
		
		log.info("We have "+ops.size()+" Operations in this definition.");		
		
		// convert all operations to Daios format
		List<IWSDLOperation> DaiosOps = new
			LinkedList<IWSDLOperation>();
		Operation op = null;
		for(Object o : ops) {
			op = (Operation) o;
			// oneway op
			if(op.getOutput().getMessage() == null)
				DaiosOps.add(new WSDLOperation(
					op.getName(), op.getInput().getMessage().getQName().getLocalPart(),
					null));
			// other
			else
				DaiosOps.add(new WSDLOperation(
					op.getName(), op.getInput().getMessage().getQName().getLocalPart(),
					op.getOutput().getMessage().getQName().getLocalPart()));
		}
		
		// return operations
		return DaiosOps;
	}

	/**
	 * Load all types. We are using an external parser (see
	 * <link>at.ac.tuwien.infosys.dsg.daios.wsdl.parser.TypeParser</link>
	 * ) to parse the XML Schemata that are contained in the WSDL definition.
	 * 
	 * @return A list of all types.
	 */
	private WSDLTypeTree retrieveTypes() {
		
		if(interceptor != null)
			interceptor.beginWSDLTypeParsing(style, wsdl, location);
			
		// use the type parser that is registered for this style
		// and get all types
		WSDLTypeTree types = TypeParser.getInstance(style.getInvocationStyle())
				.getElementTypes(wsdl, location, style.getInvocationStyle());

		if(interceptor != null)
			interceptor.finishedWSDLTypeParsing(style, wsdl, location, types);
		
		return types;
		
	}
	
	/**
	 * Get the SOAP encoding style of this service.
	 * 
	 * @return Either "document/wrapped", "document/literal" or "rpc/encoded".
	 * @throws AmbigiousException If there is no SOAP binding
	 * defined in this service.
	 */
	private WSDLEncoding retrieveStyle() throws AmbigiousException {

		// get all bindings
		Map binds = wsdl.getBindings();
		Collection col = binds.values();
		
		// if we have more than one binding - print a warning and
		// use the SOAP binding
		if(col.size() > 1)
			log.severe("This WSDL file contains "+
					col.size()+" different bindings. Will try to default to "+
					"SOAP binding (if present).");
		
		SOAPBinding binding = getDefaultBinding(col);
		
		if(binding == null)
			throw new AmbigiousException("No SOAP Binding present in this "+
				"WSDL definition.");
		
		String invStyle = binding.getStyle();
		
		// if this is RPC we return now
		if(invStyle.equals(IConstants.RPC_STYLE))
			return new WSDLEncoding(IConstants.RPC_STYLE, IConstants.ENCODED_STYLE);
		
		// for document we also check if it is literal or wrapped
		boolean wrapped = true;
		for(IWSDLMessage m : getMessages()) {
			
			if(!WSDLUtil.wrappedStyle(m)) {
				wrapped = false;
				break;
			}
			
		}
		
		if(wrapped)
			return new WSDLEncoding(invStyle, IConstants.WRAPPED_STYLE);
		else
			return new WSDLEncoding(invStyle, IConstants.LITERAL_STYLE);
			
	}
	
	/**
	 * Get the default (the SOAP) binding.
	 * 
	 * @param col A collection containing all bindings.
	 * @return The SOAP binding. Null if there is no SOAP
	 * binding in this collection.
	 */
	private SOAPBinding getDefaultBinding(Collection col) {
		
		Binding b = null;
		List exts = null;
		
		for(Object o: col) {
			b = (Binding)o;
			exts = b.getExtensibilityElements();
			for(Object ob: exts)
				if(ob instanceof SOAPBinding)
					return (SOAPBinding) ob;
		}
		
		return null;
	}
	
}
